<?php
/**
 *
 * This file provides {@link Domain51_Model_Abstract}
 *
 *
 * @package Domain51
 *
 * @subpackage Domain51_Model
 *
 * @author Travis Swicegood <development [at] domain51 [dot] com>
 *
 * @license http://opensource.org/licenses/bsd-license.php BSD
 *
 */


/**
 *
 * Load super-class to handle exceptions, locale, etc.
 *
 * @ignore
 */
Solar::loadClass('Solar_Base', true);


/**
 *
 * Load required abstract Meta object
 *
 * @ignore
 */
Solar::loadClass('Domain51_Model_Meta_Abstract', true);


/**
 *
 * An abstract version of the Model within the Domain51 MVC framework
 *
 * This provides the basic interface for reporting properties, tracking changes,
 * insuring properties are within the defined Meta information, 
 * {@link revert()}ing changes made before {@link save()} is called, and finally
 * {@link save()}ing the properties.
 *
 * None of the actual loading/saving happens within the Model, instead the
 * callback object set via {@link attach()} is provided with this object in 
 * order to determine what should be saved.
 *
 *
 * @package Domain51
 *
 * @subpackage Domain51_Model
 *
 * @author Travis Swicegood <development [at] domain51 [dot] com>
 *
 * @license http://opensource.org/licenses/bsd-license.php BSD
 *
 */
abstract class Domain51_Model_Abstract extends Solar_Base
{
    /**
     *
     * An array of properties that this Model represents as returned by 
     * {@link __get()}.
     *
     *
     * @var array
     *
     * @access protected
     */
    protected $_data = array();
    
    
    /**
     *
     * An array of all of the changes to the various properties
     *
     *
     * @var array
     *
     * @access protected
     *
     */
    protected $_changes = array();
    
    
    /**
     *
     * A link to the {@link Domain51_Model_Meta} object that represents this
     * Model.
     *
     *
     * @var array
     *
     * @access protected
     *
     */
    protected $_meta = null;
    
    
    /**
     *
     * A link to a valid callback that will be called on {@link save()}
     *
     *
     * @var mixed
     *
     * @access protected
     *
     * @see attach()
     *
     */
    protected $_callback = null;
    
    
    /**
     *
     * Handle instantiation
     *
     * This is called by {@link Domain51_Model_Mapper} when a new Model is
     * fetched.
     *
     * @param array $config An associative array of data that represents this
     * model.
     *
     * @return void
     *
     */
    public function __construct($config = null) {
        parent::__construct($config);
        
        // we must have a meta object in the config and it must be of a
        // Domain_Model_Meta_Abstract type
        if (empty($this->_config['meta'])) {
            throw $this->_exception(
                'ERR_MISSING_CONFIG_VALUE',
                array('meta')
            );
        } elseif (!($this->_config['meta'] instanceof Domain51_Model_Meta_Abstract)) {
            throw $this->_exception(
                'ERR_WRONG_CONFIG_TYPE',
                array('meta')
            );
        }
        
        // a good meta object was passed in
        $this->_meta = $this->_config['meta'];
        
        // populate _data based on meta object
        foreach ($this->_meta->columns as $value) {
            if (isset($config['properties'][$value])) {
                $this->_data[$value] = $config['properties'][$value];
            }
        }
    }
    
    
    /**
     *
     * Return the value of a given key.
     *
     * Since Model's keep track of changes without committing to persistant
     * storage, it is possible for this value to be out of sync with the 
     * persistant storage layer.  To see if this is a modified value, use
     * {@link isModified()} and specify the key
     *
     * This is a PHP magic method and is called directly by invoking:
     * <code>$model->key;</code>
     *
     * @param string $key
     *
     * @return void
     *
     */
    public function __get($key) {
        if (isset($this->_changes[$key])) {
            // return latest changed version of this
            return $this->_changes[$key];
        } elseif (isset($this->_data[$key])) {
            // return property as set at construct
            return $this->_data[$key];
        }
        
        // nothing returned
        return null;
    }
    
    
    /**
     *
     * Sets a given property to a specific value.
     *
     * Throws an exception when the specified $key is unknown.
     *
     * This is a PHP magic method, and is called directly by invoking:
     * <code>$model->key = "value";</code>
     *
     *
     * @param string $key
     *
     * @param mixed $value
     *
     * @return void
     *
     */
    public function __set($key, $value) {
        if (!in_array($key, $this->_meta->columns)) {
            throw $this->_exception('ERR_UNKNOWN_PROPERTY', array($key));
        }
        $this->_changes[$key] = $value;
    }
    
    
    /**
     *
     * Returns true if a given property is set
     *
     * This is a PHP magic method and is called directly by invoking:
     * <code>isset($model->key);</code>
     *
     *
     * @param string $key
     *
     * @return bool
     *
     */
    public function __isset($key) {
        return isset($this->_data[$key]) || isset($this->_changes[$key]);
    }
    
    
    /**
     *
     * Unset a changed property
     *
     * This will not unset properties from persistent storages.  To change the
     * type, you will need to actually set it to the type you wish to use.
     *
     * This is a PHP magic method and is called directly by invoking:
     * <code>unset($model->key);</code>
     *
     *
     * @param string $key
     *
     * @return void
     *
     */
    public function __unset($key) {
        unset($this->_changes[$key]);
    }
    
    /**
     *
     * Revert all changes to this property
     *
     *
     * @return void
     *
     */
    public function revert() {
        $this->_changes = array();
    }
    
    /**
     *
     * Returns an array of the latest data
     *
     *
     * @return array
     *
     */
    public function toArray() {
        return array_merge($this->_data, $this->_changes);
    }
    
    
    /**
     *
     * Return true if this Model has been modified.  If $key is provided, 
     * returns true only if that property has been modified.
     *
     *
     * @param string|null $key Specify which property to check if it is 
     *    modified.  Optional.
     *
     * @return bool
     *
     */
    public function isModified($key = null)
    {
        if (empty($key)) {
            return (count($this->_changes) > 0);
        } else {
            return isset($this->_changes[$key]);
        }
    }
    
    
    /**
     *
     * Return true if this Model is new
     *
     *
     * @return bool
     *
     */
    public function isNew()
    {
        return (count($this->_data) == 0);
    }
    
    
    /**
     *
     * Attaches a valid PHP callback to be invoked on {@link save()}
     *
     * Only one callback can be attached at a time.  See 
     * {@link http://www.php.net/manual/en/function.is-callable.php is_callable()}
     * for a definition of what a valid callback is.
     *
     *
     * @param mixed $callback A valid PHP callback.
     *
     * @return void
     *
     */
    public function attach($callback)
    {
        if (!is_callable($callback)) {
            throw $this->_exception(
                'ERR_INVALID_CALLBACK',
                array($callback)
            );
        }
        $this->_callback = $callback;
    }
    
    
    /**
     *
     * Saves this Model.
     *
     * If a callback has been specified by {@link attach()}, that callback is
     * invoked with this object as the only parameter.
     *
     * On save(), all changed data is persisted, and unset() will no longer 
     * revert changes.
     *
     *
     * @return void
     *
     */
    public function save()
    {
        if (!$this->isModified()) {
            return;
        }
        
        $this->_changes = array_merge(
            $this->_changes, 
            (array)call_user_func($this->_callback, $this)
        );
        
        $this->_data = array_merge($this->_data, $this->_changes);
        $this->_changes = array();
    }
}

?>